package ru.kipriz.todoly.api;

import ru.kipriz.todoly.api.entities.Entity;
import ru.kipriz.todoly.api.entities.ErrorMessage;
import ru.kipriz.todoly.api.entities.ProjectObject;
import ru.kipriz.todoly.api.entities.User;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

/**
 * <p>Copyright: Copyright (c) 2010</p>
 *
 * @author Andrey Volkov
 *         <br/>Date: 12/13/10
 */
public class Parser
{
    /**
     * Parse simple class.
     *
     * @param clazz
     * @param source
     * @param <T>
     * @return
     */
    public static <T> T parseJson(Class<T> clazz, String source)
    {
        String trim = source.trim();
        char[] chars = trim.toCharArray();

        T obj = null;
        StringBuffer value = new StringBuffer();
        StringBuffer key = new StringBuffer();
        boolean isKey = false;
        boolean isValue = false;
        boolean isSemicolon = false;
        try {
            for (int i = 0; i < chars.length; i++)
            {
                char ch = chars[i];
                switch (ch)
                {
                    case '{':
                        //start object
                        try
                        {
                            obj = clazz.newInstance();
                        } catch (Exception e)
                        {
                            e.printStackTrace();
                            throw new RuntimeException(e);
                        }
                        break;

                    case '}':
                        break;

                    case '"':
                        if (isSemicolon)
                        {
                            //start value
                            isSemicolon = false;
                        } else if (isKey)
                        {
                            isKey = false;
                        } else if (isValue)
                        {
                            callMethod(obj, key.toString(), value.toString());
                            isValue = false;
                        } else
                        {
                            isKey = true;
                            key = new StringBuffer();
                        }
                        break;

                    case ':':
                        isSemicolon = true;
                        value = new StringBuffer();
                        isValue = true;
                        break;

                    case ',':
                        if (isValue)
                        {
                            callMethod(obj, key.toString(), value.toString());
                        }
                        isKey = false;
                        isValue = false;
                        isSemicolon = false;
                        key = new StringBuffer();
                        break;


                    default:
                        if (isValue)
                        {
                            value.append(ch);
                        } else if (isKey)
                        {
                            key.append(ch);
                        }

                }
            }
        } catch (NoSuchMethodException e) {
            try {
                if (source.contains("ErrorMessage") && source.contains("ErrorCode")) {
                    ErrorMessage message = parseJson(ErrorMessage.class, source);
                    throw new AccessDeniedException(message);
                } else {
                    throw new RuntimeException(e);
                }
            } catch (AccessDeniedException ade) {
                throw ade;
            } catch (Exception ex) {
                e.printStackTrace();
                throw new RuntimeException(ex);
            }
        }
        return obj;
    }

    public static <T extends Entity> List<T> parseListJson(Class<T> clazz, String source)
    {
        String trim = source.trim();
        char[] chars = trim.toCharArray();

        List<T> list = null;
        Stack<String> listFields = new Stack<String>();
        Stack<List<T>> lists = new Stack<List<T>>();
        Stack<String> objectFields = new Stack<String>();
        Stack<T> objects = new Stack<T>();
        T obj = null;
        StringBuffer value = new StringBuffer();
        StringBuffer key = new StringBuffer();
        boolean isArray = false;
        boolean isArrayFinished = false;
        boolean isClass = false;
        boolean isKey = false;
        boolean isValue = false;
        boolean isSemicolon = false;
        try {
            for (int i = 0; i < chars.length; i++)
            {
                char ch = chars[i];
                switch (ch)
                {
                    case '{':
                        //start object
                        if (obj != null)
                        {
                            objects.push(obj);
                            objectFields.push(key.toString());
                        }
                        isValue = false;
                        isKey = false;
                        isSemicolon = false;
                        try
                        {
                            obj = clazz.newInstance();
                        } catch (Exception e)
                        {
                            e.printStackTrace();
                            throw new RuntimeException(e);
                        }
                        break;

                    case '}':
                        //finish object
                        if (isArray)
                        {
                            list.add(obj);
                        }
                        if (!objects.isEmpty())
                        {
                            obj = objects.pop();
                        }
                        isValue = false;
                        break;

                    case '"':
                        if (isSemicolon)
                        {
                            //start value
                            isSemicolon = false;
                        } else if (isKey)
                        {
                            isKey = false;
                        } else if (isValue)
                        {
                            callMethod(obj, key.toString(), value.toString());
                            isValue = false;
                        } else
                        {
                            isKey = true;
                            key = new StringBuffer();
                        }
                        break;

                    case ':':
                        isSemicolon = true;
                        value = new StringBuffer();
                        isValue = true;
                        break;

                    case ',':
                        if (isValue)
                        {
                            callMethod(obj, key.toString(), value.toString());
                        }
                        isKey = false;
                        isValue = false;
                        isSemicolon = false;
                        key = new StringBuffer();
                        break;

                    case '[':
                        isArray = true;
                        if (list != null)
                        {
                            lists.push(new ArrayList<T>(list));
                            listFields.push(key.toString());
                        }
                        list = new ArrayList<T>();
                        break;

                    case ']':
                        if (!lists.isEmpty())
                        {
                            callMethod(obj, listFields.pop(), list);
                        }
                        if (!lists.isEmpty())
                        {
                            list = lists.pop();
                        }
                        isArrayFinished = true;
                        break;
                    default:
                        if (isValue)
                        {
                            value.append(ch);
                        } else if (isKey)
                        {
                            key.append(ch);
                        }

                }
            }
        } catch (NoSuchMethodException e) {
            try {
                ErrorMessage message = parseJson(ErrorMessage.class, source);
                throw new AccessDeniedException(message);
            } catch (AccessDeniedException ade) {
                throw ade;
            } catch (Exception ex) {
                e.printStackTrace();
                throw new RuntimeException(ex);
            }
        }
        return list;
    }

    private static <T> void callMethod(T obj, String key, Object value) throws NoSuchMethodException
    {
        Method getMethod;
        try
        {
            getMethod = obj.getClass().getMethod("get" + key);
        } catch (NoSuchMethodException e) {
            //it seems to be boolean
            try {
                if (key.startsWith("is")) {
                    getMethod = obj.getClass().getMethod(key);
                } else if (key.startsWith("Is")) {
                    getMethod = obj.getClass().getMethod("is" + key.substring(2));
                } else {
                    getMethod = obj.getClass().getMethod("is" + key);
                }
            } catch (NoSuchMethodException nsme) {
                throw nsme;
            } catch (Exception e1) {
                e1.printStackTrace();
                throw new RuntimeException(e1);
            }
        } catch (SecurityException e)
        {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        Class<?> returnType = getMethod.getReturnType();
        try {
            Method setMethod;
            if (key.matches("^(is|Is)[A-Z]?.*")) {
                setMethod = obj.getClass().getMethod("set" + key.substring(2), returnType);
            } else {
                setMethod = obj.getClass().getMethod("set" + key, returnType);
            }
            if (value instanceof String) {
                String strValue = value.toString();
                if (strValue.equalsIgnoreCase("null")) {
                    return;
                }
                if (returnType.equals(int.class)) {
                    setMethod.invoke(obj, Integer.valueOf(strValue));
                } else if (returnType.equals(long.class)) {
                    setMethod.invoke(obj, Long.valueOf(strValue));
                } else if (returnType.equals(double.class)) {
                    setMethod.invoke(obj, Double.valueOf(strValue));
                } else if (returnType.equals(boolean.class)) {
                    setMethod.invoke(obj, Boolean.valueOf(strValue));
                } else if (returnType.equals(String.class)) {
                    setMethod.invoke(obj, strValue);
                }
            } else {
                setMethod.invoke(obj, value);
            }
        } catch (Exception e)
        {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    public static <T> String toJson(T object, String... fields)
    {
        Class<T> clazz = (Class<T>) object.getClass();
        StringBuffer buffer = new StringBuffer("{");
        Field[] declaredFields = clazz.getDeclaredFields();
        List<String> listOfFields = null;
        if (fields != null && fields.length != 0)
        {
            listOfFields = Arrays.asList(fields);
        }
        for (Field field : declaredFields)
        {
            String name = field.getName();
            if (fields != null && fields.length != 0)
            {
                if (!listOfFields.contains(name))
                {
                    continue;
                }
            }
            String upName = name.substring(0, 1).toUpperCase() + name.substring(1, name.length());
            buffer.append("\"").append(upName).append("\":");
            Class fieldClass = field.getType();
            Method method;
            try
            {
                method = clazz.getMethod((fieldClass.equals(boolean.class) ? "is" : "get") + upName);
            } catch (NoSuchMethodException e)
            {
                continue;
            }
            Object value = "";
            try
            {
                value = method.invoke(object);
            } catch (Exception e)
            {
                continue;
            }
            if (fieldClass.equals(String.class))
            {
                if (value == null)
                {
                    buffer.append("null");
                } else
                {
                    buffer.append("\"").append(value.toString()).append("\"");
                }
            } else if (!fieldClass.equals(List.class))
            {
                buffer.append(value);
            }
            buffer.append(",");
        }
        buffer.deleteCharAt(buffer.length() - 1);
        buffer.append("}");
        return buffer.toString();
    }
}
